package mogiksaop.qliphoth.client.models.smd;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

import net.minecraft.util.ResourceLocation;
import org.lwjgl.util.vector.Matrix4f;

import mogiksaop.qliphoth.common.Qliphoth;
import mogiksaop.qliphoth.common.util.RegexPatterns;
import mogiksaop.qliphoth.common.util.helpers.CommonHelper;
import mogiksaop.qliphoth.common.util.helpers.VectorHelper;

public class SmdAnimation {
	public final ValveStudioModel owner;
	public ArrayList<AnimFrame> frames;
	public ArrayList<Bone> bones;
	public int currentFrameIndex;

	public SmdAnimation(ValveStudioModel owner, String animationName, ResourceLocation resloc)
			throws GabeNewellException {
		this.frames = new ArrayList();
		this.bones = new ArrayList();
		this.currentFrameIndex = 0;

		this.frameIDBank = 0;

		this.owner = owner;
		this.animationName = animationName;
		if (resloc.getPath().endsWith(".bmd")) {
			loadBmdModel(resloc);
		} else {
			loadSmdAnim(resloc);
		}
		setBoneChildren();
		reform();
	}

	public int lastFrameIndex;
	public int totalFrames;
	public String animationName;
	private int frameIDBank;

	public SmdAnimation(SmdAnimation anim, ValveStudioModel owner) {
		this.frames = new ArrayList();
		this.bones = new ArrayList();
		this.currentFrameIndex = 0;
		this.frameIDBank = 0;
		this.owner = owner;
		this.animationName = anim.animationName;
		for (Bone b : anim.bones) {
			this.bones.add(new Bone(b, (b.parent != null) ? (Bone) this.bones.get(b.parent.ID) : null, null));
		}
		this.frames.addAll(
				(Collection) anim.frames.stream().map(f -> new AnimFrame(f, this)).collect(Collectors.toList()));
		this.totalFrames = anim.totalFrames;
	}

	private void loadSmdAnim(ResourceLocation resloc) throws GabeNewellException {
		InputStream inputStream = Qliphoth.proxy.getStreamForResourceLocation(resloc);
		String currentLine = null;
		int lineCount = 0;
		try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
			while ((currentLine = reader.readLine()) != null) {
				lineCount++;
				if (currentLine.startsWith("version"))
					continue;
				if (currentLine.startsWith("nodes")) {
					lineCount++;
					while (!(currentLine = reader.readLine()).startsWith("end")) {
						lineCount++;
						parseBone(currentLine, lineCount);
					}
					continue;
				}
				if (currentLine.startsWith("skeleton")) {
					startParsingAnimation(reader, lineCount, resloc);
				}
			}
		} catch (IOException e) {
			if (lineCount == -1) {
				throw new GabeNewellException("there was a problem opening the model file : " + resloc, e);
			}
			throw new GabeNewellException(
					"an error occurred reading the SMD file \"" + resloc + "\" on line #" + lineCount, e);
		}
	}

	private void loadBmdModel(ResourceLocation modelLoc) throws GabeNewellException {
		InputStream inputStream = Qliphoth.proxy.getStreamForResourceLocation(modelLoc);
		try (DataInputStream in = new DataInputStream(inputStream)) {
			byte version = in.readByte();
			assert version == 1;
			int numNodes = in.readShort();
			CommonHelper.ensureIndex(this.bones, numNodes - 1);
			for (int i = 0; i < numNodes; i++) {
				short boneID = in.readShort();
				short parentBone = in.readShort();
				String name = readNullTerm(in);
				Bone parent = (parentBone != -1) ? (Bone) this.bones.get(parentBone) : null;
				this.bones.set(boneID, new Bone(name, boneID, parent, null));
			}

			Map<Short, Map<Short, Matrix4f>> skeletonMap = new LinkedHashMap<Short, Map<Short, Matrix4f>>();

			int numSkeletons = this.totalFrames = in.readShort();
			short time;
			for (time = 0; time < numSkeletons; time = (short) (time + 1)) {
				skeletonMap.put(Short.valueOf(time), new LinkedHashMap());
				Map<Short, Matrix4f> frame = (Map) skeletonMap.get(Short.valueOf(time));
				int numBones = in.readShort();
				for (int j = 0; j < numBones; j++) {
					short boneId = in.readShort();
					float locX = in.readFloat();
					float locY = in.readFloat();
					float locZ = in.readFloat();
					float rotX = in.readFloat();
					float rotY = in.readFloat();
					float rotZ = in.readFloat();
					Matrix4f skeleton = VectorHelper.matrix4FromLocRot(locX, -locY, -locZ, rotX, -rotY, -rotZ);
					frame.put(Short.valueOf(boneId), skeleton);
				}
				short j;
				for (j = 0; j < numNodes; j = (short) (j + 1)) {
					if (!frame.containsKey(Short.valueOf(j))) {
						if (time == 0)
							throw new IOException("Missing bone definitions in first frame");
						frame.put(Short.valueOf(j), (Matrix4f) ((Map) skeletonMap.get(Short.valueOf((short) (time - 1)))).get(Short.valueOf(j)));
					}
				}

				this.frames.add(time, new AnimFrame(this));
				short i;
				for (i = 0; i < frame.size(); i = (short) (i + 1)) {
					((AnimFrame) this.frames.get(time)).addTransforms(i, (Matrix4f) frame.get(Short.valueOf(i)));
				}
			}
		} catch (IOException e) {
			throw new GabeNewellException(e);
		}
	}

	private void parseBone(String line, int lineCount) {
		String[] params = line.split("\"");
		int id = Integer.parseInt(RegexPatterns.SPACE_SYMBOL.matcher(params[0]).replaceAll(""));
		String boneName = params[1];
		int parentID = Integer.parseInt(RegexPatterns.SPACE_SYMBOL.matcher(params[2]).replaceAll(""));
		Bone parent = (parentID >= 0) ? (Bone) this.bones.get(parentID) : null;
		this.bones.add(id, new Bone(boneName, id, parent, null));
		ValveStudioModel.print(boneName);
	}

	private void startParsingAnimation(BufferedReader reader, int count, ResourceLocation resloc)
			throws GabeNewellException {
		int lineCount = count;
		int currentTime = 0;
		lineCount++;
		String currentLine = null;
		try {
			while ((currentLine = reader.readLine()) != null) {
				lineCount++;
				String[] params = RegexPatterns.MULTIPLE_WHITESPACE.split(currentLine);

				if (params[0].equalsIgnoreCase("time")) {
					currentTime = Integer.parseInt(params[1]);
					this.frames.add(currentTime, new AnimFrame(this));
					continue;
				}
				if (currentLine.startsWith("end")) {
					this.totalFrames = this.frames.size();
					ValveStudioModel.print("Total number of frames = " + this.totalFrames);
					return;
				}
				int boneIndex = Integer.parseInt(params[0]);
				float[] locRots = new float[6];
				for (int i = 1; i < 7; i++) {
					locRots[i - 1] = Float.parseFloat(params[i]);
				}
				Matrix4f animated = VectorHelper.matrix4FromLocRot(locRots[0], -locRots[1], -locRots[2], locRots[3],
						-locRots[4], -locRots[5]);
				((AnimFrame) this.frames.get(currentTime)).addTransforms(boneIndex, animated);
			}

		} catch (Exception e) {
			throw new GabeNewellException(
					"an error occurred reading the SMD file \"" + resloc + "\" on line #" + lineCount, e);
		}
	}

	public int requestFrameID() {
		int result = this.frameIDBank;
		this.frameIDBank++;
		return result;
	}

	private void setBoneChildren() {
		for (int i = 0; i < this.bones.size(); i++) {
			Bone theBone = (Bone) this.bones.get(i);
			this.bones.stream().filter(child -> (child.parent == theBone)).forEach(theBone::addChild);
		}
	}

	public void reform() {
		int rootID = this.owner.body.root.ID;
		for (AnimFrame frame : this.frames) {
			frame.fixUp(rootID, 0.0F);
			frame.reform();
		}
	}

	public void precalculateAnimation(SmdModel model) {
		for (AnimFrame frame1 : this.frames) {
			model.resetVerts();
			AnimFrame frame = frame1;
			for (int j = 0; j < model.bones.size(); j++) {
				Bone bone = (Bone) model.bones.get(j);
				Matrix4f animated = (Matrix4f) frame.transforms.get(j);
				bone.preloadAnimation(frame, animated);
			}
		}
	}

	public int getNumFrames() {
		return this.frames.size();
	}

	public void setCurrentFrame(int i) {
		if (this.lastFrameIndex != i) {
			this.currentFrameIndex = i;
			this.lastFrameIndex = i;
		}
	}

	private static String readNullTerm(DataInputStream in) throws IOException {
		StringBuilder str = new StringBuilder();
		char ch = Character.MIN_VALUE;
		do {
			if (ch != '\000')
				str.append(ch);
			ch = in.readChar();
		} while (ch != '\000');
		return str.toString();
	}
}